package com.atguigu.gulimall.product.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.atguigu.gulimall.product.service.CategoryBrandRelationService;
import com.atguigu.gulimall.product.vo.Catelog2Vo;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.atguigu.common.utils.PageUtils;
import com.atguigu.common.utils.Query;

import com.atguigu.gulimall.product.dao.CategoryDao;
import com.atguigu.gulimall.product.entity.CategoryEntity;
import com.atguigu.gulimall.product.service.CategoryService;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;


@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {

    @Autowired
    CategoryBrandRelationService categoryBrandRelationService;


    @Autowired
    StringRedisTemplate redisTemplate;

    @Autowired
    RedissonClient redisson;

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<CategoryEntity>()
        );

        return new PageUtils(page);
    }

    /**
     * 将分类数据封装成树形结构
     * @return
     */
    @Override
    public List<CategoryEntity> listWithTree() {

        //1、查询所有商品分类数据,
        // 这里可以使用baseMapper，其就是代表CategoryDao，也可以直接注入CategoryDao
        List<CategoryEntity> entities = baseMapper.selectList(null);  //查询条件为null就代表查询全部

        //2、组装数据
        //2.1 获取一级分类数据
        List<CategoryEntity> level1Menus = entities.stream().filter(CategoryEntity -> {
            //通过CategoryEntity对象的什么字段什么条件进行过滤，这里一级分类的父id是0
            return CategoryEntity.getParentCid() == 0;
        }).map(menu -> {  //每个菜单重新映射
            //递归设置当前菜单的子分类，需要传入过滤后的当前菜单和所有菜单
            menu.setChildren(getChildrens(menu,entities));
            return menu;  //处理后再返回菜单
        }).sorted((menu1,menu2)-> {
            //1.2 菜单进行升序排序，三元运算符防止空指针异常，因为sort是Integer类型，数据库中该字段可能没有值
            return (menu1.getSort()==null ? 0 : menu1.getSort()) - (menu2.getSort()==null ? 0 : menu2.getSort());
        }).collect(Collectors.toList());  //上面处理完后收集成一个List集合
        return level1Menus;
    }

    /**
     * 逻辑删除数据
     * @param longs
     */
    @Override
    public void removeMenusByIds(List<Long> longs) {
        //TODO 1、检测当前菜单是否被别的地方引用，如果菜单没有被别的地方引用，才能删除数据

        //2、逻辑删除，项目中很多数据都是不能直接删除的，所以我们使用Mybatis-plus的逻辑删除功能
        baseMapper.deleteBatchIds(longs);
    }

    /**
     * 获取分类数组
     * @param catelogId
     * @return
     */
    @Override
    public Long[] findCatelogPath(Long catelogId) {
        List<Long> paths=new ArrayList<>();
        List<Long> parentPath = findParentPath(catelogId, paths);
        //反转数组
        Collections.reverse(parentPath);
        return parentPath.toArray(new Long[parentPath.size()]);
    }

    /**
     * 修改分类信息时，同时修改分类关联的中间表的信息
     * 由于涉及多张表的操作，所以这里使用进行事务管理
     * @CacheEvict 失效模式使用，数据一修改，就删除指定缓存分区的缓存数据
     * @param category
     */
//    @Caching(evict = {
//            @CacheEvict(value = "category",key = "'getLevel1Categorys'"),
//            @CacheEvict(value = "category",key = "'getCategorysJson'")
//    })
    @CacheEvict(value = "category",allEntries = true)  //allEntries = true代表清除该分区的所有数据
//    @CachePut   //双写模式使用
    @Transactional
    @Override
    public void updateCascade(CategoryEntity category) {
        //先更新分类信息
        this.updateById(category);
        //更新品牌与分类中间表的信息
        categoryBrandRelationService.updateCatelog(category.getCatId(),category.getName());

        //TODO 修改其他与分类关联的中间表的信息

        //TODO 修改分类数据时，修改缓存数据。两种方式：1、双写模式：修改数据库数据时，同时修改缓存数据；2、失效模式：修改数据后删除缓存数据，等待下一次查询数据进行更新
    }

    /**
     * 获取全部一级分类
     * @Cacheable 表示开启缓存，它会帮我们实现缓存业务：查询时先查看缓存有没有数据，如果没有，则执行该方法，之后先缓存存储一份，再次获取是如果缓存有数据，则不执行该方法
     *              注意：
     *                  1、需要指定缓存的名称，这个缓存名就代表一个分区，一个缓存可以设置多个缓存名,用，隔开，代表存储缓存在多个分区中
     *                  2、建议缓存名用业务来划分
     *              @Cacheable 的默认行为：
     *                  1、如果缓存有数据，则默认不调用方法
     *                  2、key自动生成
     *                  3、ttl为-1，默认是缓存不过期
     *                  4、存入缓存的value值，默认是使用JDK的序列化机制，将序列化后的数据存入redis
     *              我们需要的行为：
     *                  1、key由我们自己指定
     *                      使用的是SPEL表达式，也可以写成字符串，字符串要用单引号引起来
     *                  2、ttl设置过期时间
     *                  3、存入redis的数据转成Json格式，方便跨平台
     * @return
     */
    @Cacheable(value = {"category"},key = "#root.method.name",sync = true)
    @Override
    public List<CategoryEntity> getLevel1Categorys() {
        long l = System.currentTimeMillis();
        QueryWrapper<CategoryEntity> wrapper = new QueryWrapper<>();
        wrapper.eq("parent_cid",0);
        List<CategoryEntity> categoryEntities = this.baseMapper.selectList(wrapper);
        System.out.println("消耗时间："+(System.currentTimeMillis()-l));
        System.out.println("查询数据库。。。");
        return categoryEntities;
    }

    /**
     * 使用缓存注解查询所有分类数据
     * @return
     */
    @Cacheable(value = {"category"},key = "#root.methodName")
    @Override
    public Map<Long, List<Catelog2Vo>> getCategorysJson() {
        //获取所有的分类数据
        List<CategoryEntity> allCategoryEntities = baseMapper.selectList(null);
        //获取一级分类
        List<CategoryEntity> level1Categorys = getCatelogList(allCategoryEntities,0L);
        System.out.println(level1Categorys.size());
        //key就是一级分类id，v就是二级分类
        Map<Long, List<Catelog2Vo>> categorysJson = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId(), v -> {
            //获取一级分类下的所有二级分类
            List<CategoryEntity> catelog2List = getCatelogList(allCategoryEntities,v.getCatId());
            //待封装的目标二级分类
            List<Catelog2Vo> catelog2VoList = null;
            //非空判断
            if (catelog2List != null && catelog2List.size() > 0) {
                catelog2VoList = catelog2List.stream().map(l2 -> {
                    //二级分类Vo
                    Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
                    List<CategoryEntity> catelog3List = getCatelogList(allCategoryEntities,l2.getCatId());
                    //非空判断
                    if (catelog3List != null && catelog3List.size() > 0) {
                        List<Catelog2Vo.Catelog3Vo> catelog3VoList = catelog3List.stream().map(l3 -> {
                            //三级分类Vo
                            return new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                        }).collect(Collectors.toList());
                        //向二级分类Vo设置三级分类Vo数组
                        catelog2Vo.setCatalog3List(catelog3VoList);
                    }
                    return catelog2Vo;
                }).collect(Collectors.toList());
            }
            return catelog2VoList;
        }));
        return categorysJson;
    }

    /**
     *  加入Redis缓存功能查询三级分类:
     *      一般我们都是将对象转成Json字符串存入Redis中，因为Json是跨平台的，其他语言也是能够将Json数据进行序列化与反序列化的
     * @return
     */
    public Map<Long, List<Catelog2Vo>> getCategorysJson2() {
        String catelogJson = redisTemplate.opsForValue().get("catelogJson");
        if(StringUtils.isEmpty(catelogJson)){
            System.out.println("没有缓存，向数据库查询数据。。。");
            //如果缓存没有数据，则从数据库中获取
            Map<Long, List<Catelog2Vo>> categorysJsonFromDB = getCategorysJsonFromDBWithRedisLock();
            //从数据库查询的数据直接返回即可
            return categorysJsonFromDB;
        }
        System.out.println("有缓存，直接从缓存获取");
        //将缓存数据转成指定的类型，由于TypeReference对象使用protected修饰符，所以需要有默认实现
        Map<Long, List<Catelog2Vo>> catelogList = JSON.parseObject(catelogJson, new TypeReference<Map<Long, List<Catelog2Vo>>>() {
        });
        return catelogList;
    }

    /**
     * 使用Redisson加分布式锁，锁住所有服务的所有线程
     *      问题：
     *          缓存的数据如何与数据库的数据保存一致，即数据一致性问题
     *      解决方案：
     *          1、双写模式
     *              问题：脏数据
     *                  如第一次请求向数据库修改一号的数据为1，而第二次请求向数据库修改1号的数据为2，由于网络等原因，第一次请求更新缓存比较慢，
     *                  而第二次修改了数据库的数据，同时修改了缓存，最后第一次才修改了缓存，最终导致缓存的数据是第一次改的数据，1而不是第二次改的数据
     *              解决方案：
     *                  1、加锁，保证更新数据库和更新缓存是原子性，写数据的时候保证只有一个线程在操作
     *                  2、加过期时间，如果数据可以容忍短时间的不一致，如1天，那么当缓存过期时，还会查询数据库，数据也是能保证更新
     *              总结：
     *                  无论哪种方案，都要保证数据最终一致性，即最终数据库的数据与缓存的数据还是要同步的。
     *          2、失效模式
     *              问题：脏数据
     *                  有三次请求，第一次请求向数据库更新1号的数据为1，然后删除缓存，第二次请求向数据库更新1号的数据为2，
     *                  第三次请求读缓存，读取不到，然后向数据库读取数据，这时第二次请求还未更新数据库的数据。然后第三次请求要更新缓存，
     *                  而第三次请求更新缓存在第二次请求删除缓存之后完成，这就导致缓存的数据还是1号的数据1，而不是最新的数据2
     *              解决方案：
     *                  1、加锁
     *                  2、加过期时间
     *              总结：
     *                  对应经常更新的数据，还是直接查询数据库比较好，因为加锁会耗费很多性能
     *      缓存一致性解决方案分析：
     *          1、对于用户维度数据（如订单数据，用户数据），这种并发几率非常小，一个用户也不可能每天都在更新用户数据，对于这种无需考虑缓存一致性
     *              给缓存设置过期时间，每隔一段时间主动更新即可
     *          2、对于菜单、商品介绍等基础数据，我们可以容忍大程度的缓存不一致，比如商品介绍，不一定要实时更新，也可以使用canal订阅binlog的方式
     *          3、缓存数据+过期时间也足够解决大部分业务对缓存的要求，会定期更新数据
     *          4、通过加锁保证并发读写，写写按顺序排好队，读读无所谓。加读写锁合适。（对于业务不关心脏数据，允许临时脏数据则可以不加锁）
     *          5、使用canal更新缓存，只要数据库有数据更新，canal就会察觉到变化，然后就会去更新redis，
     *              好处就是不需要我们编码处理缓存，系统会自动更新缓存，缺点就是又要开发一个中间件，额外增加一些功能
     *              扩展：
     *                  canal解决数据异构：canal会实时查看用户商品浏览记录表、购物车表等用户喜好相关的表，
     *                  根据这些表动态的生成用户推荐表。用户想看推荐的商品，就直接从用户推荐表查询出商品即可，不需要进行大量计算
     *          总结：
     *              对于实时性、一致性要求高的，而且经常修改的数据，这些数据直接从数据库获取，不要使用缓存，
     *              对于实时性、一致性不高、不经常修改的数据可以使用缓存
     *      我们缓存数据一致性的解决方案：
     *          1、缓存数据都加上过期时间，数据过期后下次查询主动更新数据
     *          2、读写数据时，加上分布式读写锁，对于偶尔写，经常读的数据是没什么影响的
     * @return
     */
    public Map<Long, List<Catelog2Vo>> getCategorysJsonFromDBWithRedissonLock() {
        //同一个名称就代表统一把锁。所以创建锁的时候要注意锁的粒度，锁粒度越小，速度越快
        //锁的粒度，如锁住的是具体的某个商品，如11号商品，12号商品，那么它们的锁名可以分别命名为product-11-lock和product-12-lock，
        // 而不能让两个商品的使用同一把锁。不然两个商品的缓存都失效了，都要去查询数据库获取真实数据，但两个线程却在争同一把锁，这明显不合理。
        RLock lock = redisson.getLock("catelog-lock");
        //加锁
        lock.lock();
        try{
            return getDataListFromDB();
        }finally {
            //释放锁
            lock.unlock();
        }
    }

    /**
     * 从数据库获取二级分类和三级分类信息，并封装成Json数据
     * 加redis分布式锁，锁住所有服务的线程
     *
     *      分析：
     *          1、删除锁
     *              问题：
     *                  1、程序出现异常，导致锁未删除，造成死锁
     *                  2、就算使用try catch，在finally删除锁，当系统断电，finally的代码未执行完，也会导致锁未删除，造成死锁
     *         2、加锁与设置过期时间不是原子操作
     *              问题：
     *                  1、设置过期时间，当系统断电，设置过期时间的代码未执行完，也会导致锁未删除，造成死锁
     *              命令：set key value NX
     *                  NX代表没有就加锁，即存储键值对，有就不加锁，最终只有一个线程加锁成功。
     *         3、加锁与过期时间一起设置，原子操作
     *              解决问题，就算断电或出异常，要么加锁失败，要么加锁成功并设置过期时间,时间单位是秒
     *              命令：set key value EX time NX
     *         4、删除锁是锁的值不是随机值
     *              问题：
     *                  有可能你的业务花费时间长，你的锁已经过期了，这时候再去删锁就是别人的锁了
     *         5、给锁设置随机值，删除之前判断是否是自己的锁，如果是，才删除，获取值与对比值不是原子操作
     *              问题:
     *                  业务执行完之后删除锁,是我的锁才进行删除，不过这种删除方式也会导致删掉的是别人的锁
     *                  比如锁的过期时间是10秒，你花了9.5秒处理完逻辑，然后向redis查询锁的时候，花了0.3秒，发现锁还存在
     *                  返回给程序的过程中，又花了0.5秒，结果这个锁已经过期了，然比人已经获得锁了，这时删掉的就是别人的锁了
     *         6、获取值与对比值不是原子操作，使用lua脚本
     *              解决了查询redis时是自己的锁，而删除时是别人的锁的问题，因为lua脚本执行要么全部成功，要么全部失败
     *         7、锁续期
     *              当业务执行时间过长，但是锁已经快要过期了，要给锁续期，最简单的方法就是将锁的过期时间延长，这样就很少出现锁过期问题
     *
     * @return
     */
    public Map<Long, List<Catelog2Vo>> getCategorysJsonFromDBWithRedisLock() {
        //锁的值，使用随机值，每个人的锁的值是随机且唯一的
        String uuid = UUID.randomUUID().toString();
        //加锁、设置过期时间并返回加锁结果
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,300L,TimeUnit.SECONDS);
        if(lock !=null && lock){
            System.out.println("获取分布式锁成功。。");
            Map<Long, List<Catelog2Vo>> dataListFromDB;
            try{
                //加锁成功，执行业务
                //设置过期时间，必须在加锁是就设置过期时间
                // redisTemplate.expire("lock",30, TimeUnit.SECONDS);
                 dataListFromDB = getDataListFromDB();
            }finally {
                //删除锁不保证原子性
//            String myLock = redisTemplate.opsForValue().get("lock");
//            if(uuid.equals(myLock)){
//                redisTemplate.delete("lock");
//            }
                //删除锁保证原子性，使用Luau脚本
                String script="if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                        "then\n" +
                        "    return redis.call(\"del\",KEYS[1])\n" +
                        "else\n" +
                        "    return 0\n" +
                        "end";
                //使用脚本执行删除，脚本执行要么成功，要么失败
                Long delete = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList("lock"), uuid);
            }
            return dataListFromDB;
        }else {
            //加锁失败，重试，类似于自旋锁
            //1、睡眠100ms再尝试
            try {
                Thread.sleep(100L);
            } catch (InterruptedException e) {

            }
            //2、重试
            System.out.println("获取分布式锁成功。。重试");
            return getCategorysJsonFromDBWithRedisLock();
        }
    }

    /**
     * 从数据库获取二级分类和三级分类信息，并封装成Json数据
     * 加本地锁，解决缓存击穿问题
     * @return
     */
    public Map<Long, List<Catelog2Vo>> getCategorysJsonFromDBWithLocalLock() {
        /**
         * 结果分析：
         *      1、加锁时，只锁住查询缓存与查询数据库，并没有锁住向redis保存缓存
         *          1、现象：
         *              本来只有一个线程会去查询数据库，但出现了两个线程去查询数据库。
         *          2、分析：
         *              这就会导致一个线程释放锁后，redis缓存并没有缓存好数据，而其他线程又向redis获取数据，结果获取不到，这就会导致其他线程会再次查询数据库
         *      2、加锁时，锁住查询缓存、查询数据库和保存缓存
         *          1、现象：
         *              只有一个线程查询数据库
         *          2、分析：
         *              同步了查询缓存、查询数据库和保存缓存，使这三步完成才释放锁
         */
        synchronized (this){
            //1、进入锁之后，再次查询缓存，有就直接返回
            return getDataListFromDB();
        }
    }

    /**
     * 从数据库获取数据，并将数据存入Redis缓存中
     * @return
     */
    private Map<Long, List<Catelog2Vo>> getDataListFromDB() {
        String catelogJson = redisTemplate.opsForValue().get("catelogJson");
        if (!StringUtils.isEmpty(catelogJson)){
            //将缓存数据转成指定的类型，由于TypeReference对象使用protected修饰符，所以需要有默认实现
            Map<Long, List<Catelog2Vo>> catelogList = JSON.parseObject(catelogJson, new TypeReference<Map<Long, List<Catelog2Vo>>>() {
            });
            return catelogList;
        }
        //2、如果没有缓存，再向数据库查询数据
        System.out.println("向数据库查询数据22");
        //获取所有的分类数据
        List<CategoryEntity> allCategoryEntities = baseMapper.selectList(null);
        //获取一级分类
        List<CategoryEntity> level1Categorys = getCatelogList(allCategoryEntities,0L);
        System.out.println(level1Categorys.size());
        //key就是一级分类id，v就是二级分类
        Map<Long, List<Catelog2Vo>> categorysJson = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId(), v -> {
            //获取一级分类下的所有二级分类
            List<CategoryEntity> catelog2List = getCatelogList(allCategoryEntities,v.getCatId());
            //待封装的目标二级分类
            List<Catelog2Vo> catelog2VoList = null;
            //非空判断
            if (catelog2List != null && catelog2List.size() > 0) {
                catelog2VoList = catelog2List.stream().map(l2 -> {
                    //二级分类Vo
                    Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
                    List<CategoryEntity> catelog3List = getCatelogList(allCategoryEntities,l2.getCatId());
                    //非空判断
                    if (catelog3List != null && catelog3List.size() > 0) {
                        List<Catelog2Vo.Catelog3Vo> catelog3VoList = catelog3List.stream().map(l3 -> {
                            //三级分类Vo
                            return new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                        }).collect(Collectors.toList());
                        //向二级分类Vo设置三级分类Vo数组
                        catelog2Vo.setCatalog3List(catelog3VoList);
                    }
                    return catelog2Vo;
                }).collect(Collectors.toList());
            }
            return catelog2VoList;
        }));
        //将数据库获取的数据向Redis存储一份
        String cache = JSON.toJSONString(categorysJson);
        redisTemplate.opsForValue().set("catelogJson",cache);
        return categorysJson;
    }

    /**
     * 找出指定的父分类id的所有分类
     * @param categoryEntities
     * @param parentId
     * @return
     */
    private List<CategoryEntity> getCatelogList(List<CategoryEntity> categoryEntities,Long parentId) {
        //return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", v.getCatId()));
        List<CategoryEntity> collect = categoryEntities.stream().filter(item -> item.getParentCid() == parentId).collect(Collectors.toList());
        return collect;
    }

    /**
     * 递归获取分类的父分类id
     * @param catelogId
     * @param paths
     * @return
     */
    private List<Long> findParentPath(Long catelogId,List<Long> paths){
        paths.add(catelogId);
        CategoryEntity categoryEntity = this.getById(catelogId);
        if(categoryEntity.getParentCid() !=0){
            findParentPath(categoryEntity.getParentCid(),paths);
        }
        return paths;
    }

    /**
     * 递归收集菜单的子菜单
     *      规则：
     *          它会先收集递归第一个n级菜单的所有子菜单，然后在按顺序递归收集第二个n级菜单，最终递归收集最后一个n级菜单
     * @param root
     * @param entities
     * @return
     */
    private List<CategoryEntity> getChildrens(CategoryEntity root,List<CategoryEntity> entities){
        List<CategoryEntity> childrens = entities.stream().filter(CategoryEntity -> {
            //收集所有菜单父id等于root菜单id的菜单
            return CategoryEntity.getParentCid() == root.getCatId();
        }).map(menu -> {
            //递归收集子菜单
            menu.setChildren(getChildrens(menu, entities));
            return menu;
        }).sorted((menu1, menu2) -> {
            return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
        }).collect(Collectors.toList());
        return childrens;
    }


}